package tinybase.rm;

import java.nio.ByteBuffer;

import junit.framework.Assert;

import tinybase.basic.ByteArrayUtility;
import tinybase.basic.BytePointer;
import tinybase.basic.RC;
import tinybase.pf.*;

//RM_FileHandle: RM File interface
//This class is used to manipulate the records in 
//an open RM component file
public class RM_FileHandle {

	PF_FileHandle pfFileHandle;
	RM_FileHdr fileHdr; // file header
	public RM_FileHdr getFileHdr() {
		return fileHdr;
	}

	boolean bHdrChanged; // dirty flag for file hdr

	public boolean isbHdrChanged() {
		return bHdrChanged;
	}

	public void setbHdrChanged(boolean bHdrChanged) {
		this.bHdrChanged = bHdrChanged;
	}

	// Constructor. Initialize instance variables
	public RM_FileHandle() {
		this.pfFileHandle= new PF_FileHandle();
		this.bHdrChanged = false;
		fileHdr = new RM_FileHdr();
		fileHdr.setFirstFree(RM_Internal.RM_PAGE_LIST_END);
	}

	//
	// GetRec
	//
	// Desc: Given a RID, return the record
	// In: rid -
	// Out: rec -
	// Ret: RM return code
	//
	public RC getRec(RID rid, RM_Record rec) {
		RC rc = RC.SUCCESS;
		PageNum refPageNum = new PageNum(PageNum.PF_PAGE_UNSET);
		Slot refSlot = new Slot(Slot.SLOT_UNSET);
		int pageNum = -1;
		int slotNum = -1;
		PF_PageHandle pageHandle = new PF_PageHandle();
		BytePointer refByteArray = new BytePointer();
		byte[] pData = null;

		// Extract page number from rid
		if ((rc = rid.getPageNum(refPageNum)) != RC.SUCCESS) {
			return rc;
		}

		// Extract slot number from rid
		if ((rc = rid.getSlotNum(refSlot)) != RC.SUCCESS) {
			return rc;
		}

		pageNum = refPageNum.getPageNum();
		slotNum = refSlot.getSlotValue();
		// Sanity Check: slotNum bound check
		// Note that PF_FileHandle.GetThisPage() will take care of pageNum
		if (slotNum >= this.fileHdr.getNumRecordsPerPage() || slotNum < 0) {
			return RC.RM_INVALIDSLOTNUM;
		}

		// Get the page where rid points
		if ((rc = pfFileHandle.getThisPage(pageNum, pageHandle)) != RC.SUCCESS) {
			return rc;
		}

		// Get the data
		if ((rc = pageHandle.getData(refByteArray)) != RC.SUCCESS) {
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}

		// Sanity Check: a record corresponding to rid should exist
		pData = refByteArray.getArray();
		if (!this.getBitmap(pData, PF_PageHandle.DATASTART_OFFSET
				+ RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
			pfFileHandle.unpinPage(pageNum);
			return RC.RM_RECORDNOTFOUND;
		}

		// Copy the record to RM_Record
		rec.setRid(rid);
//		rec.setpData(null);
		rec.setRecordSize(fileHdr.getRecordSize());
		byte[] rec_pData = new byte[rec.getRecordSize()];
		System.arraycopy(pData,
				PF_PageHandle.DATASTART_OFFSET + fileHdr.getPageHeaderSize()
						+ slotNum * fileHdr.getRecordSize(), rec_pData, 0,
				fileHdr.getRecordSize());
		rec.setpData(rec_pData);

		// Unpin the page
		if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
			return rc;
		}
		return RC.SUCCESS;

	}

	//
	// InsertRec
	//
	// Desc: Insert a new record
	// In: pData -
	// Out: rid -
	// Ret: RM return code
	//
	public RC insertRec(byte[] pRecordData, RID rid) {
		RC rc = RC.SUCCESS;
		PageNum refPageNum = new PageNum(PageNum.PF_PAGE_UNSET);
		int pageNum = PageNum.PF_PAGE_UNSET;
		int slotNum = Slot.SLOT_UNSET;
		PF_PageHandle pageHandle = new PF_PageHandle();
		BytePointer refpData = new BytePointer();
		byte[] pData;

		// Sanity Check: pRecordData must not be NULL
		if (pRecordData == null) {
			return RC.RM_NULLPOINTER;
		}

		// Allocate a new page if free page list is empty
		if (fileHdr.getFirstFree() == RM_Internal.RM_PAGE_LIST_END) {
			// Call PF_FileHandle::AllocatePage()
			if ((rc = pfFileHandle.allocatePage(pageHandle)) != RC.SUCCESS) {
				return rc;
			}

			// Get page number
			if ((rc = pageHandle.getPageNum(refPageNum)) != RC.SUCCESS) {
				return rc;
			}

			pageNum = refPageNum.getPageNum();

			// Get data pointer
			if ((rc = pageHandle.getData(refpData)) != RC.SUCCESS) {
				pfFileHandle.unpinPage(pageNum);
				return rc;
			}
			pData = refpData.getArray();

			// Set page header
			// Note that allocated page was already zeroed out
			// (We don't have to initialize the bitmap)
			ByteArrayUtility.putInt(pData, PF_PageHandle.DATASTART_OFFSET,
					RM_Internal.RM_PAGE_LIST_END);

			// Mark the page dirty since we changed the next pointer
			if ((rc = pfFileHandle.markDirty(pageNum)) != RC.SUCCESS) {
				pfFileHandle.unpinPage(pageNum);
				return rc;
			}

			// Unpin the page
			if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
				return rc;
			}

			fileHdr.setFirstFree(pageNum);
			this.setbHdrChanged(true);
		}
		// Pick the first page on the list
		else {
			pageNum = fileHdr.getFirstFree();
		}

		// Pin the page where new record will be inserted
		if ((rc = pfFileHandle.getThisPage(pageNum, pageHandle)) != RC.SUCCESS) {
			return rc;
		}

		// Get data pointer
		if ((rc = pageHandle.getData(refpData)) != RC.SUCCESS) {
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}
		pData = refpData.getArray();

		// Find an empty slot
		slotNum = Slot.SLOT_UNSET;
		int maxSlotNum = fileHdr.getNumRecordsPerPage();
		for (slotNum = 0; slotNum < maxSlotNum; slotNum++) {
			if (!this.getBitmap(pData, PF_PageHandle.DATASTART_OFFSET + RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
				break;
			}
		}
//System.out.println("slotNum is " + slotNum);
//System.out.println("MaxSlotNum is " + maxSlotNum);

		Assert.assertTrue(slotNum < maxSlotNum);

		// Assign rid
		rid.setPageNum(pageNum);
		rid.setSlotNum(slotNum);

		// Copy the given record data to the buffer pool
		int copySize = Math.min(fileHdr.getRecordSize(), pRecordData.length);
		System.arraycopy(pRecordData, 0, pData,
				PF_PageHandle.DATASTART_OFFSET + fileHdr.getPageHeaderSize()
						+ slotNum * fileHdr.getRecordSize(),
				copySize);

		// Set bit
		this.setBitmap(pData, PF_PageHandle.DATASTART_OFFSET
				+ RM_PageHdr.RM_PageHdr_SIZE, slotNum);

		// Find an empty slot
		slotNum = Slot.SLOT_UNSET;
		maxSlotNum = fileHdr.getNumRecordsPerPage();
		for (slotNum = 0; slotNum < maxSlotNum; slotNum++) {
			if (!this.getBitmap(pData, PF_PageHandle.DATASTART_OFFSET + RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
				break;
			}
		}

		// Remove the page from the free page list if necessary
		if (slotNum == fileHdr.getNumRecordsPerPage()) {
			int nextFree = ByteArrayUtility.getInt(pData,
					PF_PageHandle.DATASTART_OFFSET);
			fileHdr.setFirstFree(nextFree);
			this.setbHdrChanged(true);
			ByteArrayUtility.putInt(pData, PF_PageHandle.DATASTART_OFFSET,
					RM_Internal.RM_PAGE_FULL);
		}

		// Mark the header page as dirty because we changed bitmap at least
		if ((rc = pfFileHandle.markDirty(pageNum)) != RC.SUCCESS) {
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}

		
		// Unpin the page
		if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
			return rc;
		}

		return RC.SUCCESS;
	}

	public RC deleteRec(RID rid) {
		RC rc;
		int pageNum = PageNum.PF_PAGE_UNSET;
		int slotNum = Slot.SLOT_UNSET;
		PageNum refPageNum = new PageNum(PageNum.PF_PAGE_UNSET);
		Slot refSlotNum = new Slot(Slot.SLOT_UNSET);
		PF_PageHandle pageHandle = new PF_PageHandle();
		BytePointer refpData = new BytePointer();
		byte[] pData = null;

		// Extract page number from rid
		if ((rc = rid.getPageNum(refPageNum)) != RC.SUCCESS) {
			// Test: inviable rid
			return rc;
		}
		pageNum = refPageNum.getPageNum();

		// Extract slot number from rid
		if ((rc = rid.getSlotNum(refSlotNum)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}
		slotNum = refSlotNum.getSlotValue();

		// Sanity Check: slotNum bound check
		// Note that PF_FileHandle.GetThisPage() will take care of pageNum
		if (slotNum >= fileHdr.getNumRecordsPerPage() || slotNum < 0) {
			// Test: rid with invalid slotNum
			return (RC.RM_INVALIDSLOTNUM);
		}

		// Get the page where rid points
		if ((rc = pfFileHandle.getThisPage(pageNum, pageHandle)) != RC.SUCCESS) {
			// Test: rid with invalid pageNum
			return rc;
		}

		// Get the data
		if ((rc = pageHandle.getData(refpData)) != RC.SUCCESS) {
			// Should not happen
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}
		pData = refpData.getArray();

		// Sanity Check: a record corresponding to rid should exist
		if (!getBitmap(pData, PF_PageHandle.DATASTART_OFFSET
				+ RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
			// Test: non-existing rid
			pfFileHandle.unpinPage(pageNum);
			return (RC.RM_RECORDNOTFOUND);
		}

		// Clear bit
		clrBitmap(pData, PF_PageHandle.DATASTART_OFFSET
				+ RM_PageHdr.RM_PageHdr_SIZE, slotNum);

		// Not necessary
		// memset(pData + fileHdr.pageHeaderSize + slotNum * fileHdr.recordSize,
		// '\0', fileHdr.recordSize);
		int fromIndex = PF_PageHandle.DATASTART_OFFSET
				+ fileHdr.getPageHeaderSize() + slotNum
				* fileHdr.getRecordSize();
		java.util.Arrays.fill(pData, fromIndex,
				fromIndex + fileHdr.getRecordSize(), (byte) 0);

		// Find a not empty slot
		for (slotNum = 0; slotNum < fileHdr.getNumRecordsPerPage(); slotNum++) {
			if (getBitmap(pData, PF_PageHandle.DATASTART_OFFSET
					+ RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
				break;
			}
		}

		// Dispose the page if empty (the deleted record was the last one)
		// This will help the total number of occupied pages to be remained
		// as small as possible
		if (slotNum == fileHdr.getNumRecordsPerPage()) {
			int nextFree = ByteArrayUtility.getInt(pData,
					PF_PageHandle.DATASTART_OFFSET);
			fileHdr.setFirstFree(nextFree);
			this.setbHdrChanged(true);

			// Mark the header page as dirty
			if ((rc = pfFileHandle.markDirty(pageNum)) != RC.SUCCESS) {
				// Should not happen
				return rc;
			}

			// Unpin the page
			if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
				// Should not happen
				return rc;
			}

			// Call PF_FileHandle.DisposePage()
			return pfFileHandle.disposePage(pageNum);
		}

		// Insert the page into the free page list if necessary
		int nextFree = ByteArrayUtility.getInt(pData,
				PF_PageHandle.DATASTART_OFFSET);
		if (nextFree == RM_Internal.RM_PAGE_FULL) {
			ByteArrayUtility.putInt(pData, PF_PageHandle.DATASTART_OFFSET,
					fileHdr.getFirstFree());
			fileHdr.setFirstFree(pageNum);
			this.setbHdrChanged(true);
		}

		// Mark the header page as dirty because we changed bitmap at least
		if ((rc = pfFileHandle.markDirty(pageNum)) != RC.SUCCESS) {
			// Should not happen
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}

		// Unpin the page
		if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}

		// Return ok
		return RC.SUCCESS;
	}

	//
	// UpdateRec
	//
	// Desc: Update a record
	// In:   rec -
	// Ret:  RM return code
	//
	public RC updateRec(RM_Record rec) {
		RC rc = RC.SUCCESS;
		RID rid = new RID();
		PageNum refPageNum = new PageNum(PageNum.PF_PAGE_UNSET);
		int pageNum = PageNum.PF_PAGE_UNSET;
		Slot refSlotNum = new Slot(Slot.SLOT_UNSET);
		int slotNum = Slot.SLOT_UNSET;
		PF_PageHandle pageHandle = new PF_PageHandle();
		BytePointer refpData = new BytePointer();
		byte[] pData = null;
		BytePointer refpRecordData = new BytePointer();
		byte[] pRecordData = null;

		// Get rid
		if ((rc = rec.getRid(rid)) != RC.SUCCESS) {
			// Test: unread record
			return rc;
		}

		// Get record data
		if ((rc = rec.getData(refpRecordData)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}
		pRecordData = refpRecordData.getArray();

		// Extract page number from rid
		if ((rc = rid.getPageNum(refPageNum)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}
		pageNum = refPageNum.getPageNum();

		// Extract slot number from rid
		if ((rc = rid.getSlotNum(refSlotNum)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}

		slotNum = refSlotNum.getSlotValue();

		// Sanity Check: slotNum bound check
		// Note that PF_FileHandle.GetThisPage() will take care of pageNum
		if (slotNum >= fileHdr.getNumRecordsPerPage() || slotNum < 0) {
			// Test: apply R1's record to R2
			return (RC.RM_INVALIDSLOTNUM);
		}

		// Sanity Check: recordSize of updating record and file handle
		// must match
		if (rec.getRecordSize() != fileHdr.getRecordSize()) {
			// Test: apply R1's record to R2
			return (RC.RM_INVALIDRECSIZE);
		}

		// Get the page where rid points
		if ((rc = pfFileHandle.getThisPage(pageNum, pageHandle)) != RC.SUCCESS) {
			// Test: rid with invalid pageNum
			return rc;
		}

		// Get the data
		if ((rc = pageHandle.getData(refpData)) != RC.SUCCESS) {
			// Should not happen
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}

		// Sanity Check: a record corresponding to rid should exist
		if (!getBitmap(pData, PF_PageHandle.DATASTART_OFFSET
				+ RM_PageHdr.RM_PageHdr_SIZE, slotNum)) {
			// Test: non-existing rid
			pfFileHandle.unpinPage(pageNum);
			return (RC.RM_RECORDNOTFOUND);
		}

		// Update
		System.arraycopy(pRecordData, 0, pData,
				PF_PageHandle.DATASTART_OFFSET + fileHdr.getPageHeaderSize()
						+ slotNum * fileHdr.getRecordSize(),
				fileHdr.getRecordSize());

		// Mark the header page as dirty
		if ((rc = pfFileHandle.markDirty(pageNum)) != RC.SUCCESS) {
			// Should not happen
			pfFileHandle.unpinPage(pageNum);
			return rc;
		}

		// Unpin the page
		if ((rc = pfFileHandle.unpinPage(pageNum)) != RC.SUCCESS) {
			// Should not happen
			return rc;
		}

		// Return ok
		return RC.SUCCESS;
	}

	//
	// ForcePages
	//
	// Desc: Forces a page (along with any contents stored in this class)
	//	       from the buffer pool to disk.  Default value forces all pages.
	// In:   pageNum - 
	// Ret:  RM return code
	//
	public RC forcePages(int pageNum) {
		RC rc;

		// Write back the file header if any changes made to the header
		// while the file is open
		if (bHdrChanged) {
			PF_PageHandle pageHandle = new PF_PageHandle();
			byte[] pData = null;
			BytePointer refpData = new BytePointer();

			// Get the header page
			if ((rc = pfFileHandle.getFirstPage(pageHandle)) != RC.SUCCESS) {
				// Test: unopened(closed) fileHandle, invalid file
				return rc;
			}

			// Get a pointer where header information will be written
			if ((rc = pageHandle.getData(refpData)) != RC.SUCCESS) {
				// Should not happen
				pfFileHandle.unpinPage(pageNum);
				return rc;
			}
			pData = refpData.getArray();

			// Write the file header (to the buffer pool)
			ByteBuffer buf = ByteBuffer.allocate(20);
			buf.putInt(0, fileHdr.getFirstFree());
			buf.putInt(4, fileHdr.getRecordSize());
			buf.putInt(8, fileHdr.getNumRecordsPerPage());
			buf.putInt(12, fileHdr.getPageHeaderSize());
			buf.putInt(16, fileHdr.getNumRecords());
			byte[] tmp = buf.array();
			System.arraycopy(tmp, 0, pData, PF_PageHandle.DATASTART_OFFSET, 20);

			// Mark the header page as dirty
			if ((rc = pfFileHandle.markDirty(RM_Internal.RM_HEADER_PAGE_NUM)) != RC.SUCCESS) {
				// Should not happen
				pfFileHandle.unpinPage(pageNum);
				return rc;
			}

			// Unpin the header page
			if ((rc = pfFileHandle.unpinPage(RM_Internal.RM_HEADER_PAGE_NUM)) != RC.SUCCESS) {
				// Should not happen
				return rc;
			}

			if ((rc = pfFileHandle.forcePages(RM_Internal.RM_HEADER_PAGE_NUM)) != RC.SUCCESS) {
				// Should not happen
				return rc;
			}

			// Set file header to be not changed
			this.setbHdrChanged(false);
		}

		// Call PF_FileHandle::ForcePages()
		if ((rc = pfFileHandle.forcePages(pageNum)) != RC.SUCCESS) {
			return rc;
		}

		// Return ok
		return RC.SUCCESS;
	}

	// Bitmap Manipulation
	//
	// GetBitmap
	//
	// Desc: Return a bit corresponding to the given index
	// In:   map - address of bitmap
	//		 offset - start index of the map
	//	     idx - bit index
	// Ret:  TRUE or FALSE
	//
	boolean getBitmap(byte[] map, int offset, int idx) {
		return (map[offset + (idx / 8)] & (1 << (idx % 8))) != 0;
	}

	//
	// SetBitmap
	//
	// Desc: Set a bit corresponding to the given index
	// In:   map - address of bitmap
	//		 offset - start index of the map
	//	     idx - bit index
	//
	private void setBitmap(byte[] map, int offset, int idx) {
		map[offset + (idx / 8)] |= (1 << (idx % 8));
	}

	//
	// ClrBitmap
	//
	// Desc: Clear a bit corresponding to the given index
	// In:   map - address of bitmap
	//		 offset - start index of the map
    //	     idx - bit index
	//
	private void clrBitmap(byte[] map, int offset, int idx) {
		map[offset + (idx / 8)] &= ~(1 << (idx % 8));
	}

}
